package it.unicam.cs.cosy.bioshape.core.util;

import hermesV3.Message;
import hermesV3.agent.AbstractProtocol;
import hermesV3.agent.Agent;
import hermesV3.agent.CommunicationException;
import hermesV3.agent.Identifier;
import hermesV3.agent.InteractionContainer;
import hermesV3.protocols.SynProtocol;
import hermesV3.util.HermesLogger;

/**
 * Utility class with methods to easily set up communication among Hermes
 * {@code Agents}, in particular {@code HUB}, {@code Peer}s,
 * {@code LocalCoordinator}s and the {@code GlobalCoordinator}. The sends are
 * always <i>asynchronous</i> whereas the receives are always
 * <i>synchronous</i>, i.e., the the system waits for the reception of a
 * message.
 * 
 * Validation on reception
 * 
 * @see hermesV3.Message
 * @see hermesV3.agent.InteractionContainer
 * @see hermesV3.agent.AbstractProtocol
 * @see hermesV3.agent.Interaction
 * 
 * @author Federico Buti
 */
public class Messages {

	private static Identifier inputId;
	private Class<?> clazz;
	private static volatile Messages mes = null;

	private Messages(Identifier inputId, Class<?> clazz) {
		Messages.inputId = inputId;
		this.clazz = clazz;
	}

	/**
	 * Set the {@code Identifier} for the {@code SimulationAgent}. It is used
	 * in the method which should take in account messages from the
	 * {@code SimulationAgent} indicating that the simulation is aborted (or
	 * paused) and the {@code Agent} must act accordingly. <br>
	 * Subsequent calls to this method do not change the {@code Identifier}
	 * passed the first time (Singleton patter).
	 * 
	 * @param inputId
	 *            The {@code Identifier} of the {@code SimulationAgent},
	 *            generated at the application start up.
	 * @param clazz
	 *            The class of the {@code SimulationAgent} agent.
	 * @see <a href="http://en.wikipedia.org/wiki/Singleton_pattern">Singleton
	 *      Pattern</a>
	 */
	public static void setInput(Identifier inputId, Class<?> clazz) {
		if (mes == null) { // avoid accidental overwrite!
			mes = new Messages(inputId, clazz);
		}
	}

	/**
	 * Getter for the static instance of {@code Messages}.
	 * 
	 * @return The {@code Identifier} of the {@code SimulationAgent}.
	 * 
	 */
	private static Identifier getId() {
		return mes.inputId;
	}

	/**
	 * Getter for the static instance of {@code Messages}.
	 * 
	 * @return The {@code class} type of the {@code SimulationAgent}.
	 */
	private static Class<?> getClazz() {
		return mes.clazz;
	}

	/**
	 * Validates the message against the contained interaction and protocol.
	 * Moreover, checks that the interaction name is equal to the passed one.
	 * 
	 * @param msg
	 *            The message received.
	 * @param interaction
	 *            The specific against which validate the received message.
	 * @return The {@code Message} if validation is successful and the
	 *         interaction name match the passed one, {@code null} otherwise.
	 * @see hermesV3.Message
	 * @see hermesV3.agent.InteractionContainer
	 */
	private static Message<?> checkContent(Message<?> msg, String interaction) {
		InteractionContainer ic = (InteractionContainer) msg.getObject();
		if (ic.validate()
				&& ic.getInteractionName().equalsIgnoreCase(interaction))
			return msg;
		return null;
	}

	/**
	 * Receives a message containing a specific interaction, from an
	 * {@code Agent} to another {@code Agent}. The message is validate against
	 * the interaction.
	 * 
	 * @param receiver
	 *            The {@code Agent} receiving the message
	 * @param sender
	 *            The name of the sending {@code Agent}
	 * @param interaction
	 *            The specific interaction of the protocol, defines the content
	 *            of the message
	 * @return The {@code Message} if the reception is successful, {@code null}
	 *         otherwise
	 * @see hermesV3.Message
	 * @see hermesV3.agent.InteractionContainer
	 */
	public static Message<?> receivedInteraction(Agent receiver,
			Class<?> sender, String interaction) {
		Message<?> msg = receiver.getMessageSynch(sender.getSimpleName());
		return checkContent(msg, interaction);
	}

	/**
	 * Exactly the same as
	 * {@link #receivedInteraction(Agent, Identifier, String)} but also
	 * consider possible messages from the {@code SimulationAgent}.
	 * 
	 * @param receiver
	 *            The {@code Agent} receiving the message
	 * @param sender
	 *            The {@code Identifier} of the sending {@code Agent}
	 * @param interaction
	 *            The specific interaction of the protocol, defines the content
	 *            of the message
	 * @return The {@code Message} if the reception is successful, {@code null}
	 *         otherwise
	 * @see hermesV3.Message
	 * @see hermesV3.agent.InteractionContainer
	 */
	public static Message<?> receivedInteractionWithError(Agent receiver,
			Identifier sender, String interaction) {
		Message<?> msg = receiver.getMessageSynch(new Identifier[] { sender,
				Messages.getId() });
		if (msg.getSenderAgentId().equals(Messages.getId()))
			return null;
		return checkContent(msg, interaction);
	}

	/**
	 * Receives a message containing a specific interaction, from an
	 * {@code Agent} to another {@code Agent}. The message is validate against
	 * the interaction.
	 * 
	 * @param receiver
	 *            The {@code Agent} receiving the message
	 * @param sender
	 *            The {@code Identifier} of the sending {@code Agent}
	 * @param interaction
	 *            The specific interaction of the protocol, defines the content
	 *            of the message
	 * @return The {@code Message} if the reception is successful, {@code null}
	 *         otherwise
	 * @see hermesV3.Message
	 * @see hermesV3.agent.InteractionContainer
	 */
	public static Message<?> receivedInteraction(Agent receiver,
			Identifier sender, String interaction) {
		return receivedInteraction(receiver, new Identifier[] { sender },
				interaction);
	}

	/**
	 * Receives a message containing a specific interaction, from an
	 * {@code Agent} to another {@code Agent}. The message is validate against
	 * the interaction.
	 * 
	 * @param receiver
	 *            The {@code Agent} receiving the message
	 * @param senders
	 *            An array of {@code Identifier}s of all the {@code Agent}s
	 *            which could send the message
	 * @param interaction
	 *            The specific interaction of the protocol, defines the content
	 *            of the message
	 * @return The {@code Message} if the reception is successful, {@code null}
	 *         otherwise
	 * @see hermesV3.Message
	 * @see hermesV3.agent.InteractionContainer
	 */
	public static Message<?> receivedInteraction(Agent receiver,
			Identifier[] senders, String interaction) {
		Message<?> msg = receiver.getMessageSynch(senders);
		return checkContent(msg, interaction);
	}

	/**
	 * Exactly the same as
	 * {@link #receivedInteraction(Agent, Identifier[], String)} but also
	 * consider possible messages from the {@code SimulationAgent}.
	 * 
	 * @param receiver
	 *            The {@code Agent} receiving the message
	 * @param senders
	 *            An array of {@code Identifier}s of all the {@code Agent}s
	 *            which could send the message
	 * @param interaction
	 *            The specific interaction of the protocol, defines the content
	 *            of the message
	 * @return The {@code Message} if the reception is successful, {@code null}
	 *         otherwise
	 * @see hermesV3.Message
	 * @see hermesV3.agent.InteractionContainer
	 */
	public static Message<?> receivedInteractionWithError(Agent receiver,
			Identifier[] senders, String interaction) {
		Identifier[] ids = new Identifier[senders.length + 1];
		for (int i = 0; i < senders.length; i++)
			ids[i] = senders[i];
		ids[ids.length - 1] = Messages.getId();
		Message<?> msg = receiver.getMessageSynch(ids);
		if (msg.getSenderAgentId().equals(Messages.getId()))
			return null;
		return checkContent(msg, interaction);
	}

	/**
	 * Receives a "ack" message.
	 * 
	 * @param receiver
	 *            The {@code Agent} receiving the message
	 * @param sender
	 *            The name of the agent sending the message
	 * @return The received {@code Message} if the reception is successful,
	 *         {@code null} otherwise
	 */
	public static Message<?> receiveAck(Agent receiver, Class<?> sender) {
		return receivedInteraction(receiver, sender, SynProtocol.INTERACTIONACK);
	}

	/**
	 * Receives a "ack" message.
	 * 
	 * @param receiver
	 *            The {@code Agent} receiving the message
	 * @param sender
	 *            The {@code Identifier} of the sending {@code Agent}
	 * @return The received {@code Message} if the reception is successful,
	 *         {@code null} otherwise
	 */
	public static Message<?> receiveAck(Agent receiver, Identifier sender) {
		return receivedInteraction(receiver, new Identifier[] { sender },
				SynProtocol.INTERACTIONACK);
	}

	/**
	 * Receives a "ack" message.
	 * 
	 * @param receiver
	 *            The {@code Agent} receiving the message
	 * @param senders
	 *            An array of {@code Identifier}s of all the {@code Agent}s
	 *            which could send the message
	 * @return The received {@code Message} if the reception is successful,
	 *         {@code null} otherwise
	 */
	public static Message<?> receiveAck(Agent receiver, Identifier[] senders) {
		return receivedInteraction(receiver, senders,
				SynProtocol.INTERACTIONACK);
	}

	/**
	 * Receives a "syn" message.
	 * 
	 * @param receiver
	 *            The {@code Agent} receiving the message
	 * @param sender
	 *            The name of the agent sending the message
	 * @return The received {@code Message} if the reception is successful,
	 *         {@code null} otherwise
	 */
	public static Message<?> receiveSyn(Agent receiver, Class<?> sender) {
		return receivedInteraction(receiver, sender, SynProtocol.INTERACTIONSYN);
	}

	/**
	 * Receives a "syn" message.
	 * 
	 * @param receiver
	 *            The {@code Agent} receiving the message
	 * @param sender
	 *            The {@code Identifier} of the sending {@code Agent}
	 * @return The received {@code Message} if the reception is successful,
	 *         {@code null} otherwise
	 */
	public static Message<?> receiveSyn(Agent receiver, Identifier sender) {
		return receivedInteraction(receiver, new Identifier[] { sender },
				SynProtocol.INTERACTIONSYN);
	}

	/**
	 * Receives a "syn" message.
	 * 
	 * @param receiver
	 *            The {@code Agent} receiving the message
	 * @param senders
	 *            An array of {@code Identifier}s of all the {@code Agent}s
	 *            which could send the message
	 * @return The received {@code Message} if the reception is successful,
	 *         {@code null} otherwise
	 */
	public static Message<?> receiveSyn(Agent receiver, Identifier[] senders) {
		return receivedInteraction(receiver, senders,
				SynProtocol.INTERACTIONSYN);
	}

	/**
	 * Receives a "synack" message.
	 * 
	 * @param receiver
	 *            The {@code Agent} receiving the message
	 * @param sender
	 *            The name of the agent sending the message
	 * @return The received {@code Message} if the reception is successful,
	 *         {@code null} otherwise
	 */
	public static Message<?> receiveSynAck(Agent receiver, Class<?> sender) {
		return receivedInteraction(receiver, sender,
				SynProtocol.INTERACTIONSYNACK);
	}

	/**
	 * Receives a "synack" message.
	 * 
	 * @param receiver
	 *            The {@code Agent} receiving the message
	 * @param sender
	 *            The {@code Identifier} of the sending {@code Agent}
	 * @return The received {@code Message} if the reception is successful,
	 *         {@code null} otherwise
	 */
	public static Message<?> receiveSynAck(Agent receiver, Identifier sender) {
		return receivedInteraction(receiver, new Identifier[] { sender },
				SynProtocol.INTERACTIONSYNACK);
	}

	/**
	 * Like {{@link #receiveSynAck(Agent, Identifier)} but also accepts
	 * {@code SimulationAgent}.
	 * 
	 * @param receiver
	 *            The {@code Agent} receiving the message.
	 * @param sender
	 *            The {@code Identifier} of the sending {@code Agent}.
	 * @param error
	 *            The {@code Identifier} of the {@code SimulationAgent}.
	 * 
	 * @return The received {@code Message} if the reception is successful,
	 *         {@code null} otherwise.
	 */
	public static Message<?> receiveSynAckWithError(Agent receiver,
			Identifier sender, Identifier error) {
		Message<?> msg = receiver.getMessageSynch(new Identifier[] { sender,
				error });
		if (msg.getSenderAgentId().equals(error))
			return null;
		return checkContent(msg, SynProtocol.INTERACTIONSYNACK);
	}

	/**
	 * Like {{@link #receiveSyn(Agent, Identifier)} but also accepts
	 * {@code SimulationAgent}.
	 * 
	 * @param receiver
	 *            The {@code Agent} receiving the message.
	 * @param sender
	 *            The {@code Identifier} of the sending {@code Agent}.
	 * @param error
	 *            The {@code Identifier} of the {@code SimulationAgent}.
	 * 
	 * @return The received {@code Message} if the reception is successful,
	 *         {@code null} otherwise.
	 */
	public static Message<?> receiveSynWithError(Agent receiver,
			Identifier sender, Identifier error) {
		Message<?> msg = receiver.getMessageSynch(new Identifier[] { sender,
				error });
		if (msg.getSenderAgentId().equals(error))
			return null;
		return checkContent(msg, SynProtocol.INTERACTIONSYN);
	}

	/**
	 * Receives a "synack" message.
	 * 
	 * @param receiver
	 *            The {@code Agent} receiving the message
	 * @param senders
	 *            An array of {@code Identifier}s of all the {@code Agent}s
	 *            which could send the message
	 * @return The received {@code Message} if the reception is successful,
	 *         {@code null} otherwise
	 */
	public static Message<?> receiveSynAck(Agent receiver,
			Identifier[] senders) {
		return receivedInteraction(receiver, senders,
				SynProtocol.INTERACTIONSYNACK);
	}

	/**
	 * Sends an "ack" message.
	 * 
	 * @param sender
	 *            The {@code Agent} sending the message
	 * @param receiver
	 *            The {@code Identifier} of the receiving {@code Agent}
	 * @return {@code true} if the transmission is successful, {@code false}
	 *         otherwise
	 */
	public static boolean sendAck(Agent sender, Identifier receiver) {
		return sendAck(sender, receiver, new Object[] { "" });
	}

	/**
	 * Sends an "ack" message.
	 * 
	 * @param sender
	 *            The {@code Agent} sending the message
	 * @param receiver
	 *            The {@code Identifier} of the receiving {@code Agent}
	 * @param object
	 *            Data to be sent in the message
	 * @return {@code true} if the transmission is successful, {@code false}
	 *         otherwise
	 */
	public static boolean sendAck(Agent sender, Identifier receiver,
			Object object) {
		InteractionContainer ic = new InteractionContainer(new SynProtocol());
		ic.makeInteraction(SynProtocol.INTERACTIONACK, object);
		return sendMessage(sender, receiver, ic);
	}

	/**
	 * Sends a specific message, given the {@code InteractionContainer} with the
	 * data and the protocol already set.
	 * 
	 * @param sender
	 *            The {@code Agent} sending the message
	 * @param receiver
	 *            The {@code Identifier} of the receiving {@code Agent}
	 * @param ic
	 *            {@code InteractionContainer} with the information to be sent
	 * @return {@code true} if the transmission is successful, {@code false}
	 *         otherwise
	 * @see hermesV3.agent.InteractionContainer
	 * @see hermesV3.agent.AbstractProtocol
	 */
	public static boolean sendInteraction(Agent sender, Identifier receiver,
			InteractionContainer ic) {
		return sendMessage(sender, receiver, ic);
	}

	/**
	 * Sends a specific <i>empty</i> message, given the protocol for the
	 * {@code InteractionContainer}, the interaction name and the data of the
	 * message.
	 * 
	 * @param sender
	 *            The {@code Agent} sending the message
	 * @param receiver
	 *            The {@code Identifier} of the receiving {@code Agent}
	 * @param protocol
	 *            The specific protocol for the interaction, content will be
	 *            validated on reception with respect to the protocol
	 * @param interaction
	 *            The specific interaction of the protocol, defines the content
	 *            of the message
	 * @return {@code true} if the transmission is successful, {@code false}
	 *         otherwise
	 * @see hermesV3.agent.InteractionContainer
	 * @see hermesV3.agent.AbstractProtocol
	 */
	public static boolean sendInteraction(Agent sender, Identifier receiver,
			AbstractProtocol protocol, String interaction) {
		InteractionContainer ic = new InteractionContainer(protocol);
		ic.makeInteraction(interaction);
		return sendMessage(sender, receiver, ic);
	}

	/**
	 * Same as {
	 * {@link #sendServiceInteraction(Agent, Identifier, AbstractProtocol, String)}
	 * but the receiver must be a {@code ServiceAgent}.
	 * 
	 * @param sender
	 *            The {@code Agent} sending the message
	 * @param receiver
	 *            The {@code Identifier} of the receiving
	 *            {@code ServiceAgent}
	 * @param protocol
	 *            The specific protocol for the interaction, content will be
	 *            validated on reception with respect to the protocol
	 * @param interaction
	 *            The specific interaction of the protocol, defines the content
	 *            of the message
	 * @return {@code true} if the transmission is successful, {@code false}
	 *         otherwise
	 * @see hermesV3.agent.InteractionContainer
	 * @see hermesV3.agent.AbstractProtocol
	 */
	public static boolean sendServiceInteraction(Agent sender,
			Identifier receiver, AbstractProtocol protocol,
			String interaction) {
		return sendServiceInteraction(sender, receiver, protocol, interaction,
				new Object[] { "" });
	}

	/**
	 * Same as {
	 * {@link #sendServiceInteraction(Agent, Identifier, AbstractProtocol, String)}
	 * but the receiver must be a {@code ServiceAgent}.
	 * 
	 * @param sender
	 *            The {@code Agent} sending the message
	 * @param receiver
	 *            The {@code Identifier} of the receiving
	 *            {@code ServiceAgent}
	 * @param protocol
	 *            The specific protocol for the interaction, content will be
	 *            validated on reception with respect to the protocol
	 * @param interaction
	 *            The specific interaction of the protocol, defines the content
	 *            of the message
	 * @param object
	 *            Data to be sent in the message
	 * @return {@code true} if the transmission is successful, {@code false}
	 *         otherwise
	 * @see hermesV3.agent.InteractionContainer
	 * @see hermesV3.agent.AbstractProtocol
	 */
	public static boolean sendServiceInteraction(Agent sender,
			Identifier receiver, AbstractProtocol protocol,
			String interaction, Object object) {
		if (!receiver.isServiceAgent())
			throw new IllegalArgumentException(
					"The receiver agent must be a correct ServiceAgent");
		InteractionContainer ic = new InteractionContainer(protocol);
		ic.makeInteraction(interaction, object);
		try {
			sender.sendMessageToServiceAgent(new Message<InteractionContainer>(
					sender.getIdentifier(), receiver, ic));
		} catch (CommunicationException e) {
			HermesLogger.log(HermesLogger.ERROR, sender.getAgentName()
					+ " - unable to correctly acknowledge the "
					+ "communication with the service agent...");
			return false;
		}
		return true;
	}

	/**
	 * Sends a specific message given the protocol for the
	 * {@code InteractionContainer}, the interaction name and the data of the
	 * message.
	 * 
	 * @param sender
	 *            The {@code Agent} sending the message
	 * @param receiver
	 *            The {@code Identifier} of the receiving {@code Agent}
	 * @param protocol
	 *            The specific protocol for the interaction, content will be
	 *            validated on reception with respect to the protocol
	 * @param interaction
	 *            The specific interaction of the protocol, defines the content
	 *            of the message
	 * @param objects
	 *            List of data sent in the message
	 * @return {@code true} if the transmission is successful, {@code false}
	 *         otherwise
	 * @see hermesV3.agent.AbstractProtocol
	 */
	// TODO verificare come si comporta ad usarlo con i protocolli
	public static boolean sendInteraction(Agent sender, Identifier receiver,
			AbstractProtocol protocol, String interaction, Object... objects) {
		InteractionContainer ic = new InteractionContainer(protocol);
		ic.makeInteraction(interaction, objects);
		return sendMessage(sender, receiver, ic);
	}

	/**
	 * Executes the actual sending of the message
	 * 
	 * @param sender
	 *            The {@code Agent} sending the message
	 * @param receiver
	 *            The {@code Identifier} of the receiving {@code Agent}
	 * @param ic
	 *            {@code InteractionContainer} with the information to be sent
	 * @return {@code true} if the transmission is successful, {@code false}
	 *         otherwise
	 * @see hermesV3.agent.InteractionContainer
	 */
	private static boolean sendMessage(Agent sender, Identifier receiver,
			InteractionContainer ic) {
		try {
			sender.sendMessageToUserAgent(new Message<InteractionContainer>(
					sender.getIdentifier(), receiver, ic));
		} catch (CommunicationException e) {
			HermesLogger.log(HermesLogger.ERROR, sender.getAgentName()
					+ " - unable to correctly acknowledge the "
					+ "communication...");
			return false;
		}
		return true;
	}

	/**
	 * Sends a "syn" message.
	 * 
	 * @param sender
	 *            The {@code Agent} sending the message
	 * @param receiver
	 *            The {@code Identifier} of the receiving {@code Agent}
	 * @param object
	 *            Data to be sent in the message
	 * @return {@code true} if the transmission is successful, {@code false}
	 *         otherwise
	 */
	public static boolean sendSyn(Agent sender, Identifier receiver,
			Object object) {
		InteractionContainer ic = new InteractionContainer(new SynProtocol());
		ic.makeInteraction(SynProtocol.INTERACTIONSYN, object);
		return sendMessage(sender, receiver, ic);
	}

	/**
	 * Sends a "syn" message.
	 * 
	 * @param sender
	 *            The {@code Agent} sending the message
	 * @param receiver
	 *            The {@code Identifier} of the receiving {@code Agent}
	 * @return {@code true} if the transmission is successful, {@code false}
	 *         otherwise
	 */
	public static boolean sendSyn(Agent sender, Identifier receiver) {
		return sendSyn(sender, receiver, new Object[] { "" });
	}

	/**
	 * Sends a "synack" message.
	 * 
	 * @param sender
	 *            The {@code Agent} sending the message
	 * @param receiver
	 *            The {@code Identifier} of the receiving {@code Agent}
	 * @param object
	 *            Data to be sent in the message
	 * @return {@code true} if the transmission is successful, {@code false}
	 *         otherwise
	 */
	public static boolean sendSynAck(Agent sender, Identifier receiver,
			Object object) {
		InteractionContainer ic = new InteractionContainer(new SynProtocol());
		ic.makeInteraction(SynProtocol.INTERACTIONSYNACK, object);
		return sendMessage(sender, receiver, ic);
	}

	/**
	 * Sends a "synack" message.
	 * 
	 * @param sender
	 *            The {@code Agent} sending the message
	 * @param receiver
	 *            The {@code Identifier} of the receiving {@code Agent}
	 * @return {@code true} if the transmission is successful, {@code false}
	 *         otherwise
	 */
	public static boolean sendSynAck(Agent sender, Identifier receiver) {
		return sendSynAck(sender, receiver, new Object[] { "" });
	}

	/**
	 * Sends a specific message, given the {@code InteractionContainer} with the
	 * data and the protocol already set.
	 * 
	 * @param sender
	 *            The {@code Agent} sending the message
	 * @param receiver
	 *            The {@code Identifier} of the receiving {@code Agent}
	 * @param error
	 *            {@code String} describing the error occurred
	 * @return {@code true} if the transmission is successful, {@code false}
	 *         otherwise
	 * @see hermesV3.agent.InteractionContainer
	 * @see hermesV3.agent.AbstractProtocol
	 */
	public static boolean sendError(Agent sender, Identifier receiver,
			String error) {
		InteractionContainer ic = new InteractionContainer(new SynProtocol());
		ic.makeInteraction(SynProtocol.INTERACTIONERROR, new Object[] { error });
		return sendMessage(sender, receiver, ic);
	}

}